{T}

WebGL 进阶实战

本文是 Canvas 文档第 14 节 · WebGL 基础 的延伸。基础概念(上下文获取、着色器编译、绘制矩形)请先阅读该节,本文聚焦 3D 渲染、纹理、光照与渲染循环 等进阶主题。

1. 坐标系与矩阵变换

WebGL 的裁剪空间是 [-1, 1] 的立方体制,要渲染 3D 场景必须手动管理模型-视图-投影矩阵(MVP)。浏览器原生不提供矩阵库,通常使用 gl-matrix 或手写工具函数。

js
// 投影矩阵:透视投影,让远小近大
function perspective(out, fovy, aspect, near, far) {
  const f = 1.0 / Math.tan(fovy / 2);
  const nf = 1 / (near - far);
  out[0] = f / aspect; out[1] = 0; out[2] = 0; out[3] = 0;
  out[4] = 0; out[5] = f; out[6] = 0; out[7] = 0;
  out[8] = 0; out[9] = 0; out[10] = (far + near) * nf; out[11] = -1;
  out[12] = 0; out[13] = 0; out[14] = 2 * far * near * nf; out[15] = 0;
  return out;
}

顶点着色器中应用 MVP 矩阵:

glsl
attribute vec3 a_position;
uniform mat4 u_model;
uniform mat4 u_view;
uniform mat4 u_projection;

void main() {
  gl_Position = u_projection * u_view * u_model * vec4(a_position, 1.0);
}
提示

生产项目强烈建议直接使用 gl-matrix 而非手写矩阵,避免数值与转置顺序错误。

2. 纹理加载

纹理将图像贴到几何表面,是 3D 真实感的关键。

js
function createTexture(gl, url) {
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // 占位 1x1 像素,等待图片加载
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
    new Uint8Array([128, 128, 128, 255]));

  const image = new Image();
  image.onload = () => {
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    // 非 2 的幂尺寸需设置包裹与过滤
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  };
  image.src = url;
  return texture;
}

纹理参数速查

参数常用值作用
TEXTURE_MIN_FILTERLINEAR / NEAREST_MIPMAP_LINEAR缩小时过滤
TEXTURE_MAG_FILTERLINEAR / NEAREST放大时过滤
TEXTURE_WRAP_S/TREPEAT / CLAMP_TO_EDGE越界包裹(2 的幂尺寸才可用 REPEAT)

3. 光照基础(冯氏模型)

在片段着色器中实现简易漫反射光照:

glsl
precision mediump float;
varying vec3 v_normal;
uniform vec3 u_lightDir;

void main() {
  vec3 normal = normalize(v_normal);
  float diffuse = max(dot(normal, normalize(u_lightDir)), 0.0);
  vec3 color = vec3(0.2, 0.6, 0.9) * (0.3 + 0.7 * diffuse); // 环境 + 漫反射
  gl_FragColor = vec4(color, 1.0);
}

顶点着色器需将法线变换到世界空间并传给片段着色器(varying vec3 v_normal)。

4. 渲染循环

使用 requestAnimationFrame 驱动持续渲染,并在每一帧更新矩阵实现动画:

js
let angle = 0;
function render() {
  angle += 0.01;
  // 更新模型矩阵(绕 Y 轴旋转)
  mat4.rotateY(modelMatrix, modelMatrix, angle);
  gl.uniformMatrix4fv(u_modelLoc, false, modelMatrix);

  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
注意

启用深度测试需在初始化时调用 gl.enable(gl.DEPTH_TEST),否则 3D 物体会出现前后遮挡错误。

5. WebGL 与 Three.js 的关系

方案适用场景学习成本
原生 WebGL极致性能控制、自定义着色器、教学理解
Three.js快速搭建 3D 场景、产品级应用

对于大多数业务 3D 需求,推荐基于 Three.js 开发,仅在需要特殊渲染管线时回到原生 WebGL。

浏览器兼容性

WebGL 1.0 与 2.0 在所有现代浏览器均受支持(详见 Canvas 文档兼容性表)。移动端需注意 GPU 显存与发热控制,复杂场景建议降低分辨率或帧率。